#include <stdlib.h>

#include <common/macro/build.h>
#include <common/macro/array.h>
#include <common/macro/debug.h>
#include <common/util.h>

#include <query/blobify.h>
#include <query/condition.h>
#include <query/commands.h>
#include <query/iterator.h>
#include <query/query.h>
#include <query/result.h>
#include <transaction/transaction.h>
#include <table/table.h>

/** 
* @brief Create a new query structure
* 
* @param transaction Transaction in which the query will exist.
* 
* @return Newly allocated query structure.
*/
API_PUBLIC query_t *query_new(transaction_t *transaction) {
	query_t *query;

	query = malloc(sizeof(*query));

	query->transaction = transaction;
	query->phase = QUERY_PHASE_UNPREPARED;

	query->table = NULL;
	query->storage = NULL;

	return query;
}

/** 
* @brief Release all memory associated with the query. Most likely should be
* called after query_finalize.
* 
* @param query
* 
* @return 
*/
API_PUBLIC void query_destroy(query_t *query) {
	free(query);
}

/** 
* @brief Append a list of conditions. All conditions in one list are in logical
* conjunction.
* 
* @param query
* @param conditions_c Number of conditions in the list.
* @param conditions Array of pointers to conditions 
* 
* @return 
*/
API_PUBLIC void query_append_conditions_list(query_t *query, int conditions_c, condition_t **conditions) {
	condition_list_t *condition_list;

	condition_list = malloc(sizeof(*condition_list));
	condition_list->conditions = conditions;
	condition_list->conditions_c = conditions_c;

	ARRAY_APPEND(query->condition_lists, query->condition_lists_c, condition_list);
}

/** 
* @brief Same as query_append_conditions_list, but using vararg. The last
* item of the arguments must be NULL.
* 
* @param query
* @param ... List of conditions, terminated by NULL.
* 
* @return 
*/
API_PUBLIC void query_append_conditions(query_t *query, ...) {
	va_list ap;
	condition_t *condition;
	condition_list_t *condition_list;

	condition_list = malloc(sizeof(*condition_list));
	condition_list->conditions = NULL;
	condition_list->conditions_c = 0;

	va_start(ap, query);
	while ((condition = va_arg(ap, condition_t *)) != NULL) {
		debugmsg("iter: %p\n", condition);
		ARRAY_APPEND(condition_list->conditions, condition_list->conditions_c, condition);
	}
	va_end(ap);

	ARRAY_APPEND(query->condition_lists, query->condition_lists_c, condition_list);
}

/** 
* @brief Set values for insertion, as a list.
* 
* @param query
* @param sizes
* @param data
* 
* @return 
*/
API_PUBLIC void query_data_set_list(query_t *query, char **data_in, size_t *sizes_in) {
	size_t *sizes, size;
	char **data, *datum;
	int i;

	if (query->type != QUERY_INSERT && query->type != QUERY_UPDATE)
		return;

	sizes = NULL;
	data = NULL;

	ARRAY_ALLOC(sizes, query->table->columns_c);
	ARRAY_ALLOC(data, query->table->columns_c);

	for (i = 0; i < query->table->columns_c; i++) {
		datum = data_in[i];
		size = sizes_in[i];

		ARRAY_SET(sizes, i, size);
		if (datum != NULL) {
			ARRAY_SET(data, i, util_memdup(datum, size));
		} else {
			ARRAY_SET(data, i, NULL);
		}
	}

	if (query->insert_ctx.data_set) {
		for (i = 0; i < query->table->columns_c; i++) {
			free(query->insert_ctx.current.data[i]);
		}
		free(query->insert_ctx.current.data);
		free(query->insert_ctx.current.sizes);
	}

	query->insert_ctx.data_set = 1;
	query->insert_ctx.current.data = data;
	query->insert_ctx.current.sizes = sizes;
}

/** 
* @brief Same as query_data_set_list, but using vararg. The number of
* arguments must be the same as number of columns in the table.
* 
* @param query
* @param ... List of values pairs, first the datum, then its size.
* 
* @return 
*/
API_PUBLIC void query_data_set(query_t *query,...) {
	va_list ap;
	size_t *sizes, size;
	char **data, *datum;
	int i;

	/* Not doing DML with new data? Bail out. */
	if (query->type != QUERY_INSERT && query->type != QUERY_UPDATE)
		return;

	sizes = NULL;
	data = NULL;

	ARRAY_ALLOC(sizes, query->table->columns_c);
	ARRAY_ALLOC(data, query->table->columns_c);

	va_start(ap, query);
	for (i = 0; i < query->table->columns_c; i++) {
		datum = va_arg(ap, char *);
		size = va_arg(ap, size_t);

		ARRAY_SET(sizes, i, size);
		if (datum != NULL) {
			ARRAY_SET(data, i, util_memdup(datum, size));
		} else {
			ARRAY_SET(data, i, NULL);
		}
	}
	va_end(ap);

	if (query->insert_ctx.data_set) {
		for (i = 0; i < query->table->columns_c; i++) {
			free(query->insert_ctx.current.data[i]);
		}
		free(query->insert_ctx.current.data);
		free(query->insert_ctx.current.sizes);
	}

	query->insert_ctx.data_set = 1;
	query->insert_ctx.current.data = data;
	query->insert_ctx.current.sizes = sizes;
}

/** 
* @brief Retrieve value from database.
* 
* @param query
* @param column_id
* @param data Output argument for data.
* @param size Output argument for size.
* 
* @return 
*/
API_PUBLIC result_e query_data_get(query_t *query, int column_id, char **data, size_t *size) {
	page_number_t page_number_blob;

	if (column_id >= query->table->columns_c)
		return RESULT_INVALIDCOLUMN;

	switch (query->iterator.tuple->atoms[column_id].type) {
		case TUPLE_ATOM_BLOBLINK:
			if (query->blob_cache_data[column_id] != NULL) {
				/* Don't we have it cached? */
				*data = query->blob_cache_data[column_id];
				*size = query->blob_cache_sizes[column_id];
				return RESULT_OK;
			}

			page_number_blob = *((page_number_t*)query->iterator.tuple->atoms[column_id].data);
			query_deblobify(query, data, size, page_number_blob); /* FIXME: leak right here. Cache it. */

			/* Store it in cache. */
			query->blob_cache_data[column_id] = *data;
			query->blob_cache_sizes[column_id] = *size;
			return RESULT_OK;

		case TUPLE_ATOM_DATA:
			*data = query->iterator.tuple->atoms[column_id].data;
			*size = query->iterator.tuple->atoms[column_id].size;	
			return RESULT_OK;

		default:
			return RESULT_EIO;
	}
}

/** 
* @brief Figure out column ID based on the column name.
* 
* @param query
* @param column_name
* @param column_id Output argument for column ID.
* 
* @return 
*/
API_PUBLIC result_e query_column_get_id(query_t *query, char *column_name, int *column_id) {
	*column_id = table_get_column_id(query->table, column_name);

	if (*column_id >= query->table->columns_c || *column_id < 0)
		return RESULT_INVALIDCOLUMN;
	return RESULT_OK;
}

/** 
* @brief Retrieve data type name based on the column ID.
* 
* @param query
* @param column_id
* @param datatype Output argument for data type.
* 
* @return 
*/
API_PUBLIC result_e query_datatype_get(query_t *query, int column_id, char **datatype) {
	if (column_id >= query->table->columns_c)
		return RESULT_INVALIDCOLUMN;

	*datatype = query->table->columns[column_id]->datatype->name;
	return RESULT_OK;
}

/** 
* @brief Similar to query_datatype_get, retrieves list of raw arguments for the data type.
* 
* @param query
* @param column_id
* @param datatype_args Output argument for data type arguments.
* @param argc Output argument for count of arguments.
* 
* @return 
*/
API_PUBLIC result_e query_datatype_args_get(query_t *query, int column_id, char ***datatype_args, int *argc) {
	if (column_id >= query->table->columns_c)
		return RESULT_INVALIDCOLUMN;

	*datatype_args = query->table->columns[column_id]->dt_args_raw;
	*argc = query->table->columns[column_id]->dt_args_raw_c;
	return RESULT_OK;
}

/** 
* @brief Set callback used for each matching row during update.
* 
* @param query 
* @param funcptr Pointer to the callback function.
* @param ctx Context passed to the function (may be NULL).
* 
* @return 
*/
API_PUBLIC void query_update_callback(query_t *query, query_callback_f funcptr, void *ctx) {
	/* Not an UPDATE? The callback is useless. */
	if (query->type != QUERY_UPDATE)
		return;
	
	query->update_ctx.funcptr = funcptr;
	query->update_ctx.ctx = ctx;
}

/** 
* @brief Prepare a query.
* 
* @param query
* @param type Type of query, may be QUERY_SELECT, QUERY_INSERT, QUERY_DELETE or QUERY_UPDATE.
* @param tablename Name of the table.
* 
* @return 
*/
API_PUBLIC result_e query_prepare(query_t *query, query_type_e type, const char *tablename) {
	int i;
	table_storage_t table_storage;

	if (query->transaction->phase != ACTIVE)
		return RESULT_ETRANSACTION;

	if (query->phase != QUERY_PHASE_UNPREPARED)
		return RESULT_INVALIDCOMMAND;

	query->type = type;
	query->phase = QUERY_PHASE_PREPARED;

	if (database_table_open(query->transaction->database, tablename, &table_storage) != 0)
		return RESULT_INVALIDTABLE;
	
	query->table = table_storage.table;
	query->storage = table_storage.storage;

	query->condition_lists = NULL;
	query->condition_lists_c = 0;

	query->iterator.status = QUERY_ITERATOR_CLOSED;
	query->iterator.pointer.page_number = 0;
	query->iterator.pointer.page_item = 0;
	query->iterator.page = NULL;
	query->iterator.tuple = NULL;

	query->blob_cache_data = malloc(sizeof(*query->blob_cache_data) * query->table->columns_c);
	for (i = 0; i < query->table->columns_c; i++)
		query->blob_cache_data[i] = NULL;
	query->blob_cache_sizes = malloc(sizeof(*query->blob_cache_sizes) * query->table->columns_c);

	transaction_refresh(query->transaction);

	switch (query->type) {
		case QUERY_SELECT:
		case QUERY_DELETE:
			break;
		
		case QUERY_INSERT:
			/* Clear insert data. */
			return query_insert_prepare(query);
	
		case QUERY_UPDATE:
			/* Clear update context and insert data. */
			query->transaction->command_id++;
			if (query->transaction->command_id == 0) {
				return RESULT_TOOMANYCOMMANDS;
			}
			return query_update_prepare(query);
	}

	return RESULT_OK;
}

/** 
* @brief Execute the prepared query. All conditions etc. must be set by this point.
* 
* @param query
* 
* @return 
*/
API_PUBLIC result_e query_execute(query_t *query) {
	result_e result;

	if (query->transaction->phase != ACTIVE)
		return RESULT_ETRANSACTION;

	if (query->phase != QUERY_PHASE_PREPARED)
		return RESULT_INVALIDCOMMAND;

	debugmsg("Execute: %d\n", query->type);
	switch (query->type) {
		case QUERY_SELECT:
			result = query_select_execute(query);
			break;
	
		case QUERY_DELETE:
			result = query_delete_execute(query);
			break;

		case QUERY_UPDATE:
			result = query_update_execute(query);
			break;

		case QUERY_INSERT:
			result = RESULT_OK; /* No-op; */
			break;

		default:
			return RESULT_INVALIDCOMMAND;
	}

	/* If the query failed, fail the transaction. */
	if (result_error(result)) {
		query_finalize(query);
		transaction_to_failed(query->transaction);
	}

	query->phase = QUERY_PHASE_EXECUTED;
	return result;
}

/** 
* @brief Step through the query. This is only relevant for SELECT and INSERT.
* 
* @param query
* 
* @return 
*/
API_PUBLIC result_e query_step(query_t *query) {
	result_e result;

	if (query->transaction->phase != ACTIVE)
		return RESULT_ETRANSACTION;

	if (query->phase != QUERY_PHASE_STEPPING && query->phase != QUERY_PHASE_EXECUTED)
		return RESULT_INVALIDCOMMAND;

	switch (query->type) {
		case QUERY_SELECT:
			result = query_select_step(query);
			break;
		
		case QUERY_INSERT:
			result = query_insert_step(query);
			break;
	
		case QUERY_DELETE:
		case QUERY_UPDATE:
			return RESULT_INVALIDCOMMAND;
	}

	/* If the query failed, fail the transaction. */
	if (result_error(result)) {
		query_finalize(query);
		transaction_to_failed(query->transaction);
	}

	return result;
}

/** 
* @brief Last step of the query process -- finalize the query, flush buffers.
* 
* @param query
* 
* @return 
*/
API_PUBLIC result_e query_finalize(query_t *query) {
	int i, j;
	result_e result;

	query->phase = QUERY_PHASE_UNPREPARED;

	for (i = 0; i < query->condition_lists_c; i++) {
		for (j = 0; j < query->condition_lists[i]->conditions_c; j++) {
			condition_destroy(query->condition_lists[i]->conditions[j]);
		}
		free(query->condition_lists[i]->conditions);
		free(query->condition_lists[i]);
	}
	free(query->condition_lists);

	for (i = 0; i < query->table->columns_c; i++)
		free(query->blob_cache_data[i]);
	free(query->blob_cache_data);
	free(query->blob_cache_sizes);

	query->condition_lists = NULL;
	query->condition_lists_c = 0;

	debugmsg("Finalizing: %d\n", query->type);
	switch (query->type) {
		case QUERY_SELECT:
			result = query_select_finalize(query);
			break;
	
		case QUERY_DELETE:
			result = query_delete_finalize(query);
			break;

		case QUERY_UPDATE:
			result = query_update_finalize(query);
			break;
		
		case QUERY_INSERT:
			result = query_insert_finalize(query);
			break;

		default:
			return RESULT_INVALIDCOMMAND;
	}

	/* We postpone this error, because we always want to finalize. */
	if (query->transaction->phase != ACTIVE)
		return RESULT_ETRANSACTION;

	/* If the query failed, fail the transaction. */
	if (result_error(result))
		transaction_to_failed(query->transaction);
	
	return result;
}
